package edu.hawaii.wattdepotapps.monitor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.TimeZone;
import org.apache.wicket.PageParameters;
import org.apache.wicket.ajax.AbstractAjaxTimerBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.behavior.IBehavior;
import org.apache.wicket.model.Model;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.protocol.http.RequestUtils;
import org.apache.wicket.util.time.Duration;
import edu.hawaii.wattdepotapps.BasePage;
import org.wattdepot.client.BadXmlException;
import org.wattdepot.client.MiscClientException;
import org.wattdepot.client.NotAuthorizedException;
import org.wattdepot.client.WattDepotClient;
import org.wattdepot.resource.source.jaxb.Source;

/**
 * Monitors the latest SensorData from a selected source and outputs a specific property of the
 * users choice to DisplaySensorPanel.html. The user selects the source, interval, and data property
 * to be shown on MonitorPage.html. The DisplaySensorPanel then refreshes every interval, displaying
 * the latest SensorData of the source.
 * 
 * @author Edward Meyer, Kendyll Doi, Bao Huy Ung
 * @version 2.0
 */
public class MonitorPage extends BasePage {

  /**
   * Makes the page serializable.
   */
  private static final long serialVersionUID = -5719619758599534459L;

  /**
   * Default URI to the public server in WattDepot. Overwritten in Jetty.java if the user specifies
   * another server.
   */
  private static String restURI = "http://server.wattdepot.org:8194/wattdepot/";

  /** Default string on load for all labels. */
  private static final String DEFAULTSTRING = "---";

  /** Default string for No Sources. */
  private static final String NOSOURCES = "No Sources";

  /** Holds the connection to a WattDepot server. */
  private static WattDepotClient client;

  /** Holds and displays the drop-down menu of all sources in WattDepot. */
  private DropDownChoice<String> dropSources = null;

  /** Holds and displays the drop-down menu of all intervals for auto-updating. */
  private DropDownChoice<String> dropIntervals = null;

  /** Holds and displays the drop-down menu of all data types. */
  private DropDownChoice<String> dropTypes = null;

  /** Holds the mapping of String intervals to their Integer values in seconds. */
  private Map<String, Integer> intervalHash = new HashMap<String, Integer>();

  /** Holds the list of intervals that is used to poll WattDepot. */
  private List<String> intervals = null;

  /** Used to display the update property of a source. */
  private final Label UPDATES = new Label("UpdateOutput", new Model<String>(DEFAULTSTRING));
  /** Used to display the current sensor data of a source. */
  private final Label OUTPUT = new Label("MonitorData", new Model<String>(DEFAULTSTRING));

  /** Used to display the time when the sensor data was received into the server. */
  private final Label RECEIVED = new Label("TimeReceived", new Model<String>(DEFAULTSTRING));
  /** Used to display the last time the page checked WattDepot for new data. */
  private final Label CHECKED = new Label("LastChecked", new Model<String>(DEFAULTSTRING));
  /** Used to display the source where the data is being pulled. */
  private final Label DISPLAYSOURCE = new Label("DisplaySource", new Model<String>(DEFAULTSTRING));
  /** Used to display the chosen sensor data type. */
  private final Label DISPLAYSENSORTYPE =
      new Label("DisplaySensorType", new Model<String>(DEFAULTSTRING));
  /** Used to display the parameterized url. */
  private final TextField<String> MONITORURL = new TextField<String>("URL", new Model<String>(""));

  /** Used to hold the amount of seconds of the refresh interval. */
  private Integer refreshInteger = null;

  /**
   * Holds the refresh behavior for the DisplaySensorPanel. Initialized when user submits the form.
   */
  private IBehavior refreshTimer = null;

  /** Used to determine if the SensorPanel should be displayed again. */
  private GregorianCalendar lastChecked = null;
  /** Holds a sensordata object to get the timestamp and data of most recent poll. */
  private WattDepotLatestSensorDataCommand sensorData = null;

  private PageParameters param = new PageParameters();
  private boolean hasParams = false;
  private String paramMap1 = "Source";
  private String paramMap2 = "Type";
  private String paramMap3 = "Interval";

  /**
   * Calls the create methods of the pages interface and connects to a WattDepot. Monitors the
   * latest SensorData from a selected source and outputs a specific property of the users choice to
   * DisplaySensorPanel.html.
   * 
   * @param params The page parameter map that is passed through the url.
   */
  public MonitorPage(final PageParameters params) {

    if (!params.isEmpty()) {
      param = params;
      hasParams = true;
      
    }

    // To start the Monitor page, connect to a Wattdepot server to query data.
    // Last 2 params are null as they are used for username and password.
    connect(restURI, null, null);

    // Create the MonitorPage's UI components.
    Form<String> sources = createSourceDropDown(new Form<String>("sourceList"));
    Form<String> intervals = createIntervalDropDown(new Form<String>("intervalList"));
    Form<String> types = createTypesDropDown(new Form<String>("dataList"));

    // Initializes the HashMap used to map Interval choice to it's Integer value.
    createIntervalHash();

    // Set default for UPDATES label.
    String interval = getSourceUpdateInterval(dropSources.getDefaultModelObjectAsString());

    // Set the Update label to show the corresponding update property.
    // Check to see if the returned interval is a number. If it's not then error occurred
    // or the interval wasn't set for the source, just blank out the update label.
    try {
      Integer.parseInt(interval);
      UPDATES.setDefaultModelObject("[Updates every " + interval + " seconds]");
    }
    catch (NumberFormatException e) {
      UPDATES.setDefaultModelObject("");
    }

    // Needed for Ajax to change the label.
    UPDATES.setOutputMarkupId(true);
    OUTPUT.setOutputMarkupId(true);
    CHECKED.setOutputMarkupId(true);
    RECEIVED.setOutputMarkupId(true);
    MONITORURL.setOutputMarkupId(true);

    // Create a separate form for the display portion (right-side) of the Monitor.
    Form<Object> sensorOutput = new Form<Object>("SensorOutput");
    // Create a panel that contains all the display labels.
    final DisplaySensorPanel sensorDisplay = new DisplaySensorPanel("DisplaySensorPanel");
    sensorDisplay.add(OUTPUT);
    sensorDisplay.add(RECEIVED);
    sensorDisplay.add(CHECKED);
    sensorDisplay.add(DISPLAYSOURCE);
    sensorDisplay.add(DISPLAYSENSORTYPE);
    sensorDisplay.add(MONITORURL);
    // Add the panel to the form.
    sensorOutput.add(sensorDisplay);
    
    // Create a form for the Monitor button.
    Form<Object> submitForm = new Form<Object>("submit");
    // Add actions to the button.
    // Gets the selected options from the drop-down menus and displays
    // the output on the sensor display panel.
    submitForm.add(new Button("Submit") {

      /** Support serialization. */
      private static final long serialVersionUID = -5719619758599534459L;

      @Override
      /**
       * Gets the selected options from the drop-down menus and displays
       * the output on the sensor display panel.  Also adds a refresh timer behavior 
       * to the SensorDisplayPanel to grab the sources latest SensorData properties.
       */
      public void onSubmit() {

        if (!dropSources.getDefaultModelObjectAsString().equals(NOSOURCES)) {

          // Check to see if an Ajax timer already exists, if it does, remove it from the panel so
          // it can be replaced with a new timer.
          if (refreshTimer != null) {
            sensorDisplay.remove(refreshTimer);
          }

          // Create an array of the source name and data type to pass as an argument
          // to WattDepotLatestSensorDataCommand to retrieve sensordata and timestamp.
          final String[] monitorInfo = new String[3];
          monitorInfo[0] = dropSources.getDefaultModelObjectAsString();
          monitorInfo[1] = dropTypes.getDefaultModelObjectAsString();
          monitorInfo[2] = dropIntervals.getDefaultModelObjectAsString();

          // Get the latest SensorData for the selected source.
          sensorData = new WattDepotLatestSensorDataCommand(monitorInfo, client);

          // Update the source and sensor type fields in the output panel.
          DISPLAYSOURCE.setDefaultModelObject(sensorData.getSourceName());
          DISPLAYSENSORTYPE.setDefaultModelObject(sensorData.getDataType());

          // Output the sensor data to the OUTPUT label.
          OUTPUT.setDefaultModelObject(sensorData.getSensorData());

          // Set the checked and received labels accordingly.
          CHECKED.setDefaultModelObject(dateHourConverter(new Date().toString()));
          RECEIVED.setDefaultModelObject(timeConverter(sensorData.getLatestSensorDataTimestamp()));

          // Based on what interval is selected, get the interval in seconds from the hash map.
          refreshInteger = intervalHash.get(sensorData.getInterval());

          // Initialize last checked to right before the Ajax timer starts as a basis for
          // comparison.
          lastChecked = new GregorianCalendar();
          // Set the millisecond field to 0 to better compare 2 Gregorian types.
          // Millisecond will be too sensitive for comparing.
          lastChecked.set(Calendar.MILLISECOND, 0);

          // Initialize a new Timer behavior that will act as the refresher for the Monitor output.
          refreshTimer = new AbstractAjaxTimerBehavior(Duration.seconds(1)) {
            // Serialize this component for the sessions.
            private static final long serialVersionUID = 1L;

            @Override
            protected final void onTimer(AjaxRequestTarget target) {

              // Make a copy of the last time checked to compare if
              // the correct amount of time has passed.
              GregorianCalendar tempLastChecked = (GregorianCalendar) lastChecked.clone();
              // Create a new GregorianCalendar to compare right now.
              GregorianCalendar now = new GregorianCalendar();
              // Set the milliseconds to 0 because it becomes too sensitive to compare
              // GregorianCalendar objects (now and lastchecked).
              now.set(Calendar.MILLISECOND, 0);
              // Add the amount of time to check (refreshInteger) if it's time to update the
              // monitor.
              tempLastChecked.add(Calendar.SECOND, refreshInteger);
              tempLastChecked.set(Calendar.MILLISECOND, 0);

              // Debug print statements.
              // System.out.println("Now: " + now.toString());
              // System.out.println("Last checked: " + tempLastChecked.toString());

              // Now check if it's time to update the Monitor.
              if (now.compareTo(tempLastChecked) >= 0) {

                // Debug print statement to signify it's time to update.
                // System.out.println("~~~~~~~~~~~~~~~~~~~");

                // Reset lastChecked to now.
                lastChecked = new GregorianCalendar();
                // Update the checked and received labels to the most recent values.
                CHECKED.setDefaultModelObject(dateHourConverter(new Date().toString()));

                // Initialize a new sensorData variable and output it to the Monitor.
                sensorData = new WattDepotLatestSensorDataCommand(monitorInfo, client);
                RECEIVED.setDefaultModelObject(timeConverter(sensorData
                    .getLatestSensorDataTimestamp()));

                // Output most latest value from WattDepot.
                OUTPUT.setDefaultModelObject(sensorData.getSensorData());

                // Add the updated components to the page.
                target.addComponent(OUTPUT);
                target.addComponent(RECEIVED);
                target.addComponent(CHECKED);

              }
            }
          };
          // Add the new refreshTimer back to the SensorDisplayPanel.
          sensorDisplay.add(refreshTimer);

          // if there are no parameters, map the three possible to our page parameters
          if (param.isEmpty()) {
            param.add(paramMap1, dropSources.getDefaultModelObjectAsString());
            param.add(paramMap2, dropTypes.getDefaultModelObjectAsString());
            param.add(paramMap3, dropIntervals.getDefaultModelObjectAsString());
          }
          else {
            // if there are parameters from a previous pass, clear them then add new parameters
            param.clear();
            param.add(paramMap1, dropSources.getDefaultModelObjectAsString());
            param.add(paramMap2, dropTypes.getDefaultModelObjectAsString());
            param.add(paramMap3, dropIntervals.getDefaultModelObjectAsString());
          }

          //set text field to parameterized url for portable links.
          String url = (String) this.urlFor(MonitorPage.class, param);
          MONITORURL.setDefaultModelObject(RequestUtils.toAbsolutePath(url));
          
        }
      }
    });

    // Add all components to the page.
    add(sensorOutput);
    add(UPDATES);
    add(types);
    add(intervals);
    add(sources);
    add(submitForm);
  }

  /**
   * Connect method to create connection to WattDepot Server.
   * 
   * @param uri contains a string of the uri to connect to WattDepot.
   * @param username a string of a user on the WattDepot.
   * @param password a string of the user's password to WattDepot.
   * @return a string with output of method.
   */
  private String connect(String uri, String username, String password) {

    client = new WattDepotClient(uri, username, password);
    // Stop right away if we cannot contact server.
    if (!client.isHealthy()) {
      return "Failed to connect to " + client.getWattDepotUri();
    }
    // Indicate successful connection.
    return "Connected to " + client.getWattDepotUri();
  }

  /**
   * Creates the drop-down of data types. Types are hard-coded as Power Consumed, Power Generated,
   * Energy Consumed to Date, and Energy Generated to Date.
   * 
   * @param form Holds the data types drop-down form.
   * @return a Form<Object> object with data types added.
   */
  private Form<String> createTypesDropDown(Form<String> form) {
    // List of all possible data types to choose from.
    List<String> dataTypes =
        Arrays.asList("Power Consumed", "Power Generated", "Energy Consumed to Date",
            "Energy Generated to Date");

    // Initialize the drop-down menu field.
    // If the page has specific parameters, set the drop-down menu to the specific type.
    // Else, just use the first type in the list.
    if (hasParams) {
      dropTypes =
          new DropDownChoice<String>("DataType", new Model<String>(param.getString(paramMap2)),
              dataTypes);
    }
    else {
      dropTypes =
          new DropDownChoice<String>("DataType", new Model<String>(dataTypes.get(0)), dataTypes);
    }

    // When the user selects another data type, reset the default choice accordingly.
    dropTypes.add(new AjaxFormComponentUpdatingBehavior("onchange") {

      /** Support serialization. */
      private static final long serialVersionUID = 1L;

      @Override
      /**
       * Set the default choice to the one that was just selected.
       * 
       * @param target is the Ajax target of the onchange action.
       */
      protected void onUpdate(AjaxRequestTarget target) {
        dropTypes.setDefaultModelObject(dropTypes.getDefaultModelObject());
      }

    });

    // Add the menu to the page.
    form.add(dropTypes);

    return form;

  }

  /**
   * Creates the drop-down of update intervals. Intervals are hard-coded as 5 sec, 10 sec, 15 sec,
   * 30 sec, 1 min, 5 min, 10 min, 15 min, and 30 min.
   * 
   * @param form Holds the interval drop-down form.
   * @return a Form<Object> object with intervals added.
   */
  private Form<String> createIntervalDropDown(Form<String> form) {
    // List of all possible intervals to choose from.
    intervals =
        Arrays.asList("5 sec", "10 sec", "15 sec", "30 sec", "1 min", "5 min", "10 min", "15 min",
            "30 min");

    // Initialize the drop-down menu field.
    // If the page has specific parameters, set the drop-down menu to the specific interval.
    // Else, just use the first interval in the list.
    if (hasParams) {
      dropIntervals =
          new DropDownChoice<String>("Interval", new Model<String>(param.getString(paramMap3)),
              intervals);
    }
    else {
      dropIntervals =
          new DropDownChoice<String>("Interval", new Model<String>(intervals.get(0)), intervals);
    }

    // When the user selects another interval, reset the default choice accordingly.
    dropIntervals.add(new AjaxFormComponentUpdatingBehavior("onchange") {

      /** Support serialization. */
      private static final long serialVersionUID = 1L;

      @Override
      /**
       * Set the default choice to the one that was just selected.
       * 
       * @param target is the Ajax target of the onchange action.
       */
      protected void onUpdate(AjaxRequestTarget target) {
        dropIntervals.setDefaultModelObject(dropIntervals.getDefaultModelObject());
      }

    });

    // Add the menu to the page.
    form.add(dropIntervals);
    return form;
  }

  /**
   * Creates the DropDownMenu of all sources the client has access to in the WattDepot server. If
   * any problems occur, prints an error message on the console and sets the drop-down menu to
   * "No Sources".
   * 
   * @param form Holds the page's form.
   * @return a Form<String> object with objects added.
   */
  private Form<String> createSourceDropDown(Form<String> form) {

    // Holds the list of all sources.
    List<String> sourceList;

    // Attempt to get the list of all sources in the client.
    try {
      sourceList = getClientSources();
    }
    // Server does not allow anonymous/public access.
    catch (NotAuthorizedException e) {
      sourceList = Arrays.asList(NOSOURCES);
      System.err.format("Error: Client %s does not allow public/anonymous access.", client
          .getWattDepotUri());
    }
    // Received bad XML from server.
    catch (BadXmlException e) {
      sourceList = Arrays.asList(NOSOURCES);
      System.err.println("Error: Received bad XML from server.");
    }
    // Any other miscellaneous errors that occur will be caught by MiscClientException.
    catch (MiscClientException e) {
      sourceList = Arrays.asList(NOSOURCES);
      System.err.println("Error: Had problems retrieving sources from server, "
          + "please check your connection.");
    }

    // Initialize the drop-down menu field.
    // If the page has specific parameters, set the drop-down menu to the specific source.
    // Else, just use the first source in the list.
    if (hasParams) {
      dropSources =
          new DropDownChoice<String>("Source", new Model<String>(param.getString(paramMap1)),
              sourceList);
    }
    else {
      dropSources =
          new DropDownChoice<String>("Source", new Model<String>(sourceList.get(0)), sourceList);
    }

    // Output the update interval for any source selected.
    dropSources.add(new AjaxFormComponentUpdatingBehavior("onchange") {

      /** Support serialization. */
      private static final long serialVersionUID = 1L;

      @Override
      /**
       * For each source, get the update interval property and set the update label to display.
       * 
       * @param target is the Ajax target of the onchange action.
       */
      protected void onUpdate(AjaxRequestTarget target) {

        // Get the interval for the chosen source.
        String interval = getSourceUpdateInterval(dropSources.getDefaultModelObjectAsString());

        // Check to see if the returned interval is a number. If it's not then error occurred
        // or the interval wasn't set for the source, just blank out the update label.
        try {
          Integer.parseInt(interval);
          UPDATES.setDefaultModelObject("[Updates every " + interval + " seconds]");
        }
        catch (NumberFormatException e) {
          UPDATES.setDefaultModelObject("");
        }

        // Add the updated component to the page.
        target.addComponent(UPDATES);
      }
    });

    // Add the menu to the page.
    form.add(dropSources);

    return form;
  }

  /**
   * Returns a list of strings of all the names in a WattDepot server.
   * 
   * @return A list of String names of all sources.
   * @throws MiscClientException If error is encountered retrieving sources, or some unexpected
   * problem is encountered.
   * @throws BadXmlException If error is encountered unmarshalling the XML from the server.
   * @throws NotAuthorizedException If the client is not authorized to retrieve sources.
   */
  private List<String> getClientSources() throws NotAuthorizedException, BadXmlException,
      MiscClientException {

    // Create a list of all sources in the WattDepot server.
    List<Source> allSources = client.getSources();
    List<String> names = new ArrayList<String>();

    // Loop through each source and add the name to the list.
    for (Source source : allSources) {
      names.add(source.getName());
    }

    // Return all the names in the list.
    return names;
  }

  /**
   * Returns a string representation of a Source's update interval property.
   * 
   * @param sourceParam contains the name of the source.
   * @return a String containing the update interval.
   */
  private String getSourceUpdateInterval(String sourceParam) {
    // Create a source from the string in args and get the update property.
    try {
      Source source = client.getSource(sourceParam);
      return source.getProperty("updateInterval");
    }
    // Catch Exception if anything goes wrong during the recursion process.
    catch (Exception e) {
      return "Error retrieving update interval.";
    }
  }

  /**
   * Initializes intervalHash HashMap that maps the drop-down string intervals to it's Integer
   * value. Accommodates for seconds, minutes, and hours in the interval list. E.x. "10 seconds"
   * will map to the integer "10", minutes will also be converted to seconds.
   */
  private void createIntervalHash() {
    // Add each interval and it's corresponding integer value, in seconds, to the HashMap.
    for (String stringInterval : intervals) {

      // Get the unit of time of the interval.
      Integer unitInterval = stringInterval.indexOf(" ");
      // Unit of time starts after the first space.
      String timeUnit = stringInterval.substring(unitInterval + 1);
      // Get the substring including the space after the number because
      // some intervals have a single digit at the beginning, and it
      // will throw off the substring going from 0 to 0.
      String timeAsString = stringInterval.substring(0, unitInterval);
      // Get rid of any trailing spaces.
      timeAsString = timeAsString.replace(" ", "");

      // Determine whether the interval is in sec, min, or hr, then all the string to the hash, and
      // also the appropriate integer value in seconds.
      if ("sec".equals(timeUnit)) {
        intervalHash.put(stringInterval, Integer.parseInt(timeAsString));

      }
      else if ("min".equals(timeUnit)) {
        intervalHash.put(stringInterval, Integer.parseInt(timeAsString) * 60);
      }
      else if ("hr".equals(timeUnit)) {
        intervalHash.put(stringInterval, Integer.parseInt(timeAsString) * 3600);
      }
    }
  }

  /**
   * Converts the time stamp returned from a Wattdepot call for the last known update into a date
   * object format. If passed a null date, outputs an error message on the page.
   * 
   * @param date Timestamp of Wattdepot call in string type.
   * @return timestamp in date format and 12hr time.
   */
  private String timeConverter(String date) {

    // If the date passed is null, have the Monitor display an error message.
    if (date == null) {
      return "Error: Connection Failure";
    }

    // Break up XMLGregorianTimeStamp object into date parts.
    // required to form a valid date object.
    Integer year = Integer.parseInt(date.substring(0, 4));
    Integer month = Integer.parseInt(date.substring(5, 7));
    Integer day = Integer.parseInt(date.substring(8, 10));
    String time = date.substring(11, 19);
    Integer hour = Integer.parseInt(time.substring(0, 2));
    Integer min = Integer.parseInt(time.substring(3, 5));
    Integer sec = Integer.parseInt(time.substring(6, 8));
    // GMT timezone string to get 3-letter timezone code instead of GMT offset.
    String zone = "GMT" + Integer.parseInt(date.substring(23, 26));

    // Create a new calendar object and set the fields from the extracted date above.
    Calendar lastReceived = Calendar.getInstance();
    lastReceived.set(year, (month - 1), day, hour, min, sec);
    lastReceived.setTimeZone(TimeZone.getTimeZone(zone));

    // using the Calendar create a Date object.
    // Allows text representation of Day of week and month based of the date.
    Date convertedDate = lastReceived.getTime();

    // Convert the date object into a string for conversion into 12hr time.
    String timeStamp = convertedDate.toString();

    return dateHourConverter(timeStamp);
  }

  /**
   * Converts the 24hr time format into a 12 hr time format.
   * 
   * @param time A 24hr time format to be converted into 12hr time as a string.
   * @return time in 12hr format.
   */
  private String dateHourConverter(String time) {

    // Extract beginning until hour and everything after to switch to 12hr time.
    Integer hour = Integer.parseInt(time.substring(11, 13));
    String begString = time.substring(0, 11);
    String minsec = time.substring(13, 19);
    // String endString = time.substring(19);
    if (hour > 12) {
      hour = hour - 12;
      return begString + hour + minsec + " PM ";
    }

    // return a new string in Date format.
    return begString + hour + minsec + " AM ";
  }
  
  /**
   * Sets the hostURI to connect to.
   * 
   * @param newURI Is the new URI to the server.
   */
  public static void setRestURI (String newURI) {
    restURI = newURI;
  }
}

/**
 * Creates a panel that displays latest SensorData output of a WattDepot source.
 * 
 * @author Bao Huy Ung, Edward Meyer, Kendyll Doi
 * @version 1.0
 */
class DisplaySensorPanel extends Panel {

  /**
   * Makes the page serializable.
   */
  private static final long serialVersionUID = -5719619758599534459L;

  /**
   * Creates a panel for the output.
   * 
   * @param id Holds name of panel.
   */
  public DisplaySensorPanel(String id) {
    super(id);
  }

}
